﻿using System.Collections.Generic;
using System.Linq;
using StyleCop;
using StyleCop.CSharp;

namespace Jsl.StyleCop.CSharp.MaintainabilityRules
{
    /// <summary>
    /// Checks to make sure the field is not <c>null</c> before disposing it.
    /// </summary>
    /// <remarks>
    /// The Dispose method should be able to be called multiple times without exceptions. If the Dispose method sets a field
    /// to <c>null</c> it should make sure to check that it's not <c>null</c> before disposing it. This would cause an
    /// exception if the Dispose method were called more than once.
    /// </remarks>
    public class CheckForNullInDispose : CsRuleBase<SourceAnalyzer>
    {
        private List<LiteralExpression> accessedFields;
        private List<string> nulledFields;

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="CheckForNullInDispose"/> class.
        /// </summary>
        /// <param name="analyzer">The analyzer.</param>
        public CheckForNullInDispose(RuleAnalyzer analyzer)
            : base(analyzer)
        {
        }
        #endregion Constructors

        #region Public Methods
        /// <summary>
        /// Analyzes the document.
        /// </summary>
        public override void AnalyzeDocument()
        {
            // Exit now if the rule is disabled.
            if (!this.Analyzer.IsRuleEnabled(this.Document, RuleAnalyzer.CheckForNullInDispose))
            {
                return;
            }

            this.Document.WalkDocument(this.ElementChecker);
        }

        /// <summary>
        /// Visits the expression.
        /// </summary>
        /// <param name="expression">The expression.</param>
        /// <param name="parentExpression">The parent expression.</param>
        /// <param name="parentStatement">The parent statement.</param>
        /// <param name="parentElement">The parent element.</param>
        /// <param name="context">The context.</param>
        /// <returns>Always returns <c>true</c>.</returns>
        public bool ExpressionVisitor(
            Expression expression,
            Expression parentExpression,
            Statement parentStatement,
            CsElement parentElement,
            object context)
        {
            // If this is an assignment statement to a field then add it to the list.
            var assignmentExpression = expression as AssignmentExpression;
            if (assignmentExpression != null)
            {
                if (assignmentExpression.OperatorType == AssignmentExpression.Operator.Equals)
                {
                    if (assignmentExpression.RightHandSide.IsNullLiteral())
                    {
                        var memberLiteral = assignmentExpression.LeftHandSide.RemoveThisAccessor() as LiteralExpression;
                        if (memberLiteral != null)
                        {
                            // Weed out local variables.
                            if (!expression.Variables.Contains(memberLiteral.Text))
                            {
                                this.nulledFields.Add(memberLiteral.Text);
                            }
                        }
                    }
                }
            }

            var memberAccessExpression = expression as MemberAccessExpression;
            if (memberAccessExpression != null)
            {
                var memberLiteral = memberAccessExpression.LeftHandSide.RemoveThisAccessor() as LiteralExpression;
                if ((memberLiteral != null)
                    && !memberLiteral.IsLiteralBase()
                    && !memberLiteral.IsLiteralThis())
                {
                    // Weed out local variables.
                    if (!expression.Variables.Contains(memberLiteral.Text))
                    {
                        this.accessedFields.Add(memberLiteral);
                    }
                }
            }

            return true;
        }
        #endregion Public Methods

        #region Private Methods
        private static bool IsDisposeMethod(CsElement element)
        {
            return element.Name == "method Dispose";
        }

        private bool ElementChecker(CsElement element, CsElement parentElement, object context)
        {
            // Only process the Dispose method.
            if (!IsDisposeMethod(element))
            {
                return true;
            }

            // Process this Dispose method collecting the nulled fields and field references.
            this.nulledFields = new List<string>();
            this.accessedFields = new List<LiteralExpression>();
            element.WalkElement(null, null, this.ExpressionVisitor);

            this.ProcessFieldAccesses(element);

            return true;
        }

        private void ProcessFieldAccess(CsElement element, LiteralExpression literalExpression)
        {
            // Find all the IFs that enclose this expression.
            var ancestorIfs = literalExpression.AncestorsToElement().OfType<IfStatement>();

            // Walk up our ancestor tree looking for null check.
            foreach (var ancestorIf in ancestorIfs)
            {
                // Check if the IF has the null check we are looking for.
                var foundNullCheck = false;
                var result = ancestorIf.ConditionExpression.EvaluateExpression(
                    condition =>
                    {
                        Expression checkedExpression;
                        if (condition.IsNullCheck(RelationalExpression.Operator.NotEqualTo, out checkedExpression))
                        {
                            var literal = checkedExpression.RemoveThisAccessor() as LiteralExpression;
                            if ((literal != null)
                                && (literal.Text == literalExpression.Text))
                            {
                                foundNullCheck = true;
                                return true;
                            }
                        }

                        return null;
                    });

                // If we found a null check surrounding the field access then we are OK.
                if (result.HasValue
                    && result.Value
                    && foundNullCheck)
                {
                    return;
                }
            }

            // If we get to here, then there was no null check so add the violation.
            this.AddViolation(
                element,
                literalExpression.LineNumber,
                RuleAnalyzer.CheckForNullInDispose,
                literalExpression.Text);
        }

        private void ProcessFieldAccesses(CsElement element)
        {
            // Find all the field accesses for fields that were nulled.
            var fieldsThatWereNulled =
                from fieldAccess in this.accessedFields
                where this.nulledFields.Contains(fieldAccess.Text)
                select fieldAccess;

            // Make sure each field access is inside an IF checking if the field is not null.
            foreach (var fieldAccess in fieldsThatWereNulled)
            {
                this.ProcessFieldAccess(element, fieldAccess);
            }
        }
        #endregion Private Methods
    }
}